import sys
import os
import threading
import queue
import serial
import serial.tools.list_ports
import binascii
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from tkinter.scrolledtext import ScrolledText
from datetime import datetime

try:
    from intelhex import IntelHex
except ImportError:
    IntelHex = None

try:
    import pandas as pd
except ImportError:
    pd = None


# ====================== 系统配置 ======================
def get_available_ports():
    ports = serial.tools.list_ports.comports()
    return [port.device for port in ports]


COM_PORTS = get_available_ports() or [f"COM{i}" for i in range(1, 9)]
DEFAULT_BAUDRATES = [9600, 19200, 38400, 57600, 115200, 230400, 460800]
DATA_FORMATS = ['HEX', 'ASCII', '二进制文件']
STYLE_CONFIG = {
    'progress.red': {'background': '#ff4444', 'troughcolor': '#ffe6e6'},
    'progress.yellow': {'background': '#ffd700', 'troughcolor': '#fff8e6'},
    'progress.green': {'background': '#44ff44', 'troughcolor': '#e6ffe6'}
}


# ====================== CRC计算函数 ======================
def calculate_crc(data):
    """CRC-16/MODBUS 校验计算"""
    crc = 0xFFFF
    for byte in data:
        crc ^= byte
        for _ in range(8):
            if crc & 0x0001:
                crc = (crc >> 1) ^ 0xA001
            else:
                crc >>= 1
    return crc.to_bytes(2, 'little')


# ====================== 串口通道类 ======================
class SerialChannel:
    def __init__(self, port, baudrate):
        self.port = port
        self.baudrate = baudrate
        self.ser = None
        self.rx_queue = queue.Queue()
        self.tx_queue = queue.Queue()
        self.running = False
        self.progress = 0
        self.status = "等待连接"
        self.error_code = 0
        self.queue_lock = threading.Lock()

    def open(self):
        try:
            self.ser = serial.Serial(
                port=self.port,
                baudrate=self.baudrate,
                bytesize=serial.EIGHTBITS,
                parity=serial.PARITY_NONE,
                stopbits=serial.STOPBITS_ONE,
                timeout=0.1
            )
            self.running = True
            threading.Thread(target=self._rx_thread, daemon=True).start()
            threading.Thread(target=self._tx_thread, daemon=True).start()
            self.status = "已连接"
            return True
        except serial.SerialException as e:
            self.status = f"连接失败: {str(e)}"
            return False

    def close(self):
        self.running = False
        if self.ser and self.ser.is_open:
            self.ser.close()
        self.status = "已关闭"

    def _rx_thread(self):
        while self.running:
            try:
                if self.ser.in_waiting > 0:
                    data = self.ser.read(self.ser.in_waiting)
                    with self.queue_lock:
                        self.rx_queue.put(data)
            except Exception as e:
                self.status = f"接收错误: {str(e)}"
                break

    def _tx_thread(self):
        while self.running:
            try:
                if not self.tx_queue.empty():
                    with self.queue_lock:
                        data = self.tx_queue.get()
                    self.ser.write(data)
            except Exception as e:
                self.status = f"发送错误: {str(e)}"
                break


# ====================== 烧录核心类 ======================
class BurnManager:
    def __init__(self):
        self.channels = {port: SerialChannel(port, 115200) for port in COM_PORTS}
        self.file_data = b''
        self.current_format = '二进制文件'
        self.current_baudrate = 115200
        self.burn_lock = threading.Lock()

    def set_format(self, fmt):
        self.current_format = fmt

    def set_baudrate(self, baudrate):
        self.current_baudrate = baudrate

    def _load_data(self, file_path):
        try:
            if self.current_format == '二进制文件':
                with open(file_path, 'rb') as f:
                    return f.read()

            elif self.current_format == 'HEX':
                if not IntelHex:
                    raise ImportError("请安装intelhex库：pip install intelhex")
                ih = IntelHex(file_path)
                return ih.tobinarray()

            elif self.current_format == 'ASCII':
                with open(file_path, 'r', encoding='utf-8') as f:
                    return f.read().encode('utf-8')

            raise ValueError("未知格式")
        except binascii.Error as e:
            raise ValueError(f"HEX格式错误: {str(e)}")
        except UnicodeDecodeError as e:
            raise ValueError(f"文本编码错误: {str(e)}")
        except Exception as e:
            raise ValueError(f"数据加载失败: {str(e)}")

    def start_burn(self, port, file_path):
        if not self.burn_lock.acquire(blocking=False):
            raise Exception("烧录正在进行中")

        def burn_task():
            try:
                self.file_data = self._load_data(file_path)
                total = len(self.file_data)
                packet_size = 512
                self.channels[port].baudrate = self.current_baudrate

                for i in range(0, total, packet_size):
                    if not self.channels[port].running:
                        break

                    chunk = self.file_data[i:i + packet_size]
                    header = i.to_bytes(4, 'big')
                    crc = calculate_crc(chunk)
                    packet = header + chunk + crc

                    with self.channels[port].queue_lock:
                        self.channels[port].tx_queue.put(packet)

                    progress = (i + packet_size) / total * 100
                    self.channels[port].progress = min(progress, 100)

                    if not self._wait_ack(port):
                        raise TimeoutError("响应超时")

                self.channels[port].status = "烧录完成" if self.channels[port].running else "已中止"
            except Exception as e:
                self.channels[port].status = f"错误: {str(e)}"
            finally:
                self.burn_lock.release()

        threading.Thread(target=burn_task, daemon=True).start()

    def _wait_ack(self, port, timeout=1):
        start = datetime.now()
        while (datetime.now() - start).total_seconds() < timeout:
            with self.channels[port].queue_lock:
                if not self.channels[port].rx_queue.empty():
                    ack = self.channels[port].rx_queue.get()
                    return ack == b'\x06'
        return False


# ====================== 主界面类 ======================
class BurnerApp(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("智能串口烧录工具-V5.0")
        self.geometry("1920x1080")
        self._set_dpi_awareness()
        self.burn_mgr = BurnManager()
        self._configure_styles()
        self._setup_ui()
        self._update_ui()
        self.protocol("WM_DELETE_WINDOW", self.on_close)

    def _set_dpi_awareness(self):
        if sys.platform.startswith('win'):
            from ctypes import windll
            windll.shcore.SetProcessDpiAwareness(1)
            self.tk.call('tk', 'scaling', 1.5)

    def _configure_styles(self):
        style = ttk.Style()
        style.theme_use('clam')
        style.configure('.', font=('微软雅黑', 10))
        style.configure('TButton', padding=8, width=12)
        style.configure('TCombobox', padding=6)
        style.configure('Treeview', rowheight=28, font=('宋体', 9))
        style.configure('Treeview.Heading', font=('微软雅黑', 10, 'bold'))
        style.map('TButton',
                  background=[('active', '#e1e1e1'), ('!active', '#f8f8f8')],
                  bordercolor=[('active', '#cccccc')])

        for color in ['red', 'yellow', 'green']:
            style.configure(f'{color}.Horizontal.TProgressbar',
                            **STYLE_CONFIG[f'progress.{color}'])

    def _setup_ui(self):
        main_frame = ttk.Frame(self, padding=20)
        main_frame.pack(fill=tk.BOTH, expand=True)

        # 控制面板
        control_frame = ttk.LabelFrame(main_frame, text="控制中心", padding=15)
        control_frame.pack(fill=tk.X, pady=10)

        # 网格布局
        control_frame.grid_columnconfigure([0, 2, 4, 6, 8], weight=1, uniform="col_group")
        control_frame.grid_columnconfigure([1, 3, 5, 7], minsize=200, uniform="widget_group")

        # 第一行组件
        ttk.Label(control_frame, text="目标端口:").grid(row=0, column=0, sticky='e', padx=5)
        self.port_combo = ttk.Combobox(control_frame, values=COM_PORTS, width=18, state="readonly")
        self.port_combo.current(0)
        self.port_combo.grid(row=0, column=1, sticky='ew', padx=5)

        ttk.Label(control_frame, text="波特率:").grid(row=0, column=2, sticky='e', padx=5)
        self.baud_combo = ttk.Combobox(control_frame, values=DEFAULT_BAUDRATES, width=12, state="readonly")
        self.baud_combo.current(4)
        self.baud_combo.grid(row=0, column=3, sticky='ew', padx=5)
        self.baud_combo.bind("<<ComboboxSelected>>", self._update_baudrate)

        ttk.Label(control_frame, text="数据格式:").grid(row=0, column=4, sticky='e', padx=5)
        self.format_combo = ttk.Combobox(control_frame, values=DATA_FORMATS, width=12, state="readonly")
        self.format_combo.current(2)
        self.format_combo.grid(row=0, column=5, sticky='ew', padx=5)
        self.format_combo.bind("<<ComboboxSelected>>", lambda e: self.burn_mgr.set_format(self.format_combo.get()))

        # 按钮行
        btn_frame = ttk.Frame(control_frame)
        btn_frame.grid(row=1, column=0, columnspan=9, sticky='ew', pady=10)
        btn_frame.grid_columnconfigure([0, 1, 2, 3, 4, 5], weight=1, uniform="btn_cols")

        ttk.Button(btn_frame, text="连接端口", command=self._connect_port) \
            .grid(row=0, column=0, padx=10, sticky='ew')
        ttk.Button(btn_frame, text="开始烧录", command=self._start_burn) \
            .grid(row=0, column=1, padx=10, sticky='ew')
        ttk.Button(btn_frame, text="紧急停止", command=self._stop_burn) \
            .grid(row=0, column=2, padx=10, sticky='ew')
        ttk.Button(btn_frame, text="全部停止", command=self._stop_all_ports) \
            .grid(row=0, column=3, padx=10, sticky='ew')
        ttk.Button(btn_frame, text="清空日志", command=self._clear_logs) \
            .grid(row=0, column=4, padx=10, sticky='ew')
        ttk.Button(btn_frame, text="导出日志", command=self._export_logs) \
            .grid(row=0, column=5, padx=10, sticky='ew')

        # 文件选择
        file_frame = ttk.Frame(control_frame)
        file_frame.grid(row=2, column=0, columnspan=9, sticky='ew', pady=5)
        file_frame.grid_columnconfigure(1, weight=1)

        ttk.Label(file_frame, text="文件路径:").grid(row=0, column=0, padx=5)
        self.file_entry = ttk.Entry(file_frame)
        self.file_entry.grid(row=0, column=1, sticky='ew', padx=5)
        ttk.Button(file_frame, text="浏览文件", command=self._select_file) \
            .grid(row=0, column=2, padx=5)

        # 状态监控
        status_frame = ttk.LabelFrame(main_frame, text="实时状态", padding=15)
        status_frame.pack(fill=tk.BOTH, expand=True, pady=10)

        self.status_tree = ttk.Treeview(
            status_frame,
            columns=('port', 'baudrate', 'status', 'progress', 'errors'),
            show='headings',
            height=8
        )
        self.status_tree.column('port', width=200, anchor='center')
        self.status_tree.column('baudrate', width=200, anchor='center')
        self.status_tree.column('status', width=400, anchor='w')
        self.status_tree.column('progress', width=200, anchor='center')
        self.status_tree.column('errors', width=200, anchor='center')

        for col in ['port', 'baudrate', 'status', 'progress', 'errors']:
            self.status_tree.heading(col, text=col.capitalize())
        self.status_tree.pack(fill=tk.BOTH, expand=True)

        # 进度条
        self.progress = ttk.Progressbar(
            main_frame,
            style='green.Horizontal.TProgressbar',
            length=1800,
            mode='determinate'
        )
        self.progress.pack(fill=tk.X, pady=15)

        # 日志系统
        log_frame = ttk.LabelFrame(main_frame, text="操作日志", padding=15)
        log_frame.pack(fill=tk.BOTH, expand=True, pady=10)
        self.log_text = ScrolledText(
            log_frame,
            wrap=tk.WORD,
            state='disabled',
            font=('Consolas', 10),
            height=12
        )
        self.log_text.pack(fill=tk.BOTH, expand=True)

    def _update_baudrate(self, event):
        try:
            baud = int(self.baud_combo.get())
            self.burn_mgr.set_baudrate(baud)
            self._log(f"波特率更新为: {baud} bps")
        except ValueError:
            messagebox.showerror("错误", "无效的波特率设置")
            self.baud_combo.set(self.burn_mgr.current_baudrate)

    def _select_file(self):
        file_types = [
            ('二进制文件', '*.bin'),
            ('HEX文件', '*.hex *.txt'),
            ('文本文件', '*.txt'),
            ('所有文件', '*.*')
        ]
        path = filedialog.askopenfilename(filetypes=file_types)
        if path:
            self.file_entry.delete(0, tk.END)
            self.file_entry.insert(0, path)

    def _connect_port(self):
        port = self.port_combo.get()
        if self.burn_mgr.channels[port].open():
            self._log(f"端口 {port} 连接成功")
        else:
            messagebox.showerror("连接错误", f"无法连接 {port}")

    def _start_burn(self):
        port = self.port_combo.get()
        file_path = self.file_entry.get()

        if not file_path:
            messagebox.showwarning("输入错误", "请先选择烧录文件")
            return

        try:
            self.burn_mgr.start_burn(port, file_path)
            self._log(
                f"开始烧录 - 端口:{port} 格式:{self.burn_mgr.current_format} 波特率:{self.burn_mgr.current_baudrate}")
        except Exception as e:
            messagebox.showerror("操作错误", str(e))

    def _stop_burn(self):
        port = self.port_combo.get()
        self.burn_mgr.channels[port].close()
        self._log(f"紧急停止端口 {port}")

    def _stop_all_ports(self):
        """停止所有端口连接"""
        for port in COM_PORTS:
            self.burn_mgr.channels[port].close()
        self._log("已停止所有端口连接")

    def _clear_logs(self):
        """清空操作日志"""
        self.log_text.configure(state='normal')
        self.log_text.delete(1.0, tk.END)
        self.log_text.configure(state='disabled')
        self._log("操作日志已清空")

    def _export_logs(self):
        """导出日志到Excel文件"""
        if not pd:
            messagebox.showerror("导出错误", "请先安装pandas库：pip install pandas openpyxl")
            return

        log_content = self.log_text.get(1.0, tk.END).splitlines()
        logs = []
        for line in log_content:
            if not line.strip():
                continue
            time_part, _, message = line.strip().partition('] ')
            timestamp = time_part[1:]  # 去除开头的[
            logs.append({'时间': timestamp, '操作记录': message})

        if not logs:
            messagebox.showinfo("导出提示", "没有可导出的日志内容")
            return

        try:
            # 生成标准时间戳文件名
            timestamp_str = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
            default_filename = f"{timestamp_str}-导出日志.xlsx"

            file_path = filedialog.asksaveasfilename(
                defaultextension=".xlsx",
                initialfile=default_filename,
                filetypes=[("Excel文件", "*.xlsx"), ("所有文件", "*.*")]
            )

            if file_path:
                # 确保文件扩展名正确
                if not file_path.lower().endswith('.xlsx'):
                    file_path += '.xlsx'

                df = pd.DataFrame(logs)
                df.to_excel(file_path, index=False)
                self._log(f"日志已导出到：{file_path}")
                messagebox.showinfo("导出成功", "日志文件导出完成")
        except Exception as e:
            messagebox.showerror("导出错误", f"导出失败：{str(e)}")
            self._log(f"日志导出失败：{str(e)}")

    def _update_ui(self):
        for port in COM_PORTS:
            channel = self.burn_mgr.channels[port]
            items = self.status_tree.get_children()

            values = (
                port,
                channel.baudrate,
                channel.status,
                f"{channel.progress:.1f}%",
                channel.error_code
            )

            if port not in items:
                self.status_tree.insert('', 'end', iid=port, values=values)
            else:
                self.status_tree.item(port, values=values)

            if channel.progress <= 33:
                style = 'red'
            elif 34 <= channel.progress <= 66:
                style = 'yellow'
            else:
                style = 'green'
            self.progress.configure(style=f'{style}.Horizontal.TProgressbar')

        self.after(500, self._update_ui)

    def _log(self, message):
        timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        self.log_text.configure(state='normal')
        self.log_text.insert(tk.END, f"[{timestamp}] {message}\n")
        self.log_text.configure(state='disabled')
        self.log_text.see(tk.END)

    def on_close(self):
        for port in COM_PORTS:
            self.burn_mgr.channels[port].close()
        if hasattr(self, 'after_id'):
            self.after_cancel(self.after_id)
        self.destroy()


if __name__ == "__main__":
    app = BurnerApp()
    app.mainloop()